package task;

import com.graphhopper.util.DistanceCalc;
import com.graphhopper.util.DistancePlaneProjection;
import edu.buaa.act.helper.Utils;
import edu.buaa.act.model.GPSPoint;
import edu.buaa.act.helper.CsvGPSFileReader;
import edu.buaa.act.helper.RawDataCleaning;
import org.slf4j.Logger;

import java.io.File;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.Semaphore;
import java.util.concurrent.atomic.AtomicInteger;

import static edu.buaa.act.helper.CsvGPSFileReader.*;

/**
 * Created by T7810 on 2017/5/27.
 *
 * Usage:
 *   java -jar temporal-model.jar task.CalcNewUnicity path-to-trajectory-file space-range-in-meter time-range-in-second select-points-count repeat-calc-times
 *
 * Example:
     *   java -cp temporal-model.jar task.CalcNewUnicity D:\trajectory-file.txt 200 3600 2 10
 *
 * Explain:
 * This would read data file D:\trajectory-file.txt ,
 * set space range radius to 200 meters,
 * set temporal range to 3600 seconds,
 * will select 2 points,
 * repeat this calculation 10 times.
 *
 */
public class CalcNewUnicity
{
    private static Logger log = Utils.getDebugLogger();
    private static Random random = new Random(System.currentTimeMillis());
    private static DistanceCalc distanceCalc = new DistancePlaneProjection();

    public static void main(String[] args)
    {

        try {
            String filePath = args[0];
            int r = Integer.parseInt(args[1]); // radius of geography anonymous range (in meter)
            int t = Integer.parseInt(args[2])*1000; // temporal anonymous range (in second)
            int k = Integer.parseInt(args[3]); // sample k points.
            int q = Integer.parseInt(args[4]); // repeat q times.
            int parallelCount = Integer.parseInt(args[5]);

            log.debug("file: {}", filePath);
            log.debug("point count {}, time interval {}, r {}, repeat {} times, using {} CPUs", k, t, r, q, parallelCount);

            List<List<GPSPoint>> data = readData(new File(filePath));

            float sum = 0;
            for (int i = 0; i < q; i++) {
                float u = unicity(data, k, t, r, parallelCount);
                sum += u;
                log.debug(String.format("Unicity %f", u));
            }
            log.info("(point count, time interval, r, AVG Unicity of {} time): {} {} {} {}", q, k, t, r, sum / q);
        }catch (Throwable e){
            e.printStackTrace();
        }
    }

    public static List<List<GPSPoint>> readData(File file) throws IOException
    {
        List<GPSPoint> allData = CsvGPSFileReader.readLargeFile(file, " ", CAR_ID, LATITUDE, LONGITUDE, TIMESTAMP);
        List<List<GPSPoint>> result = RawDataCleaning.separateTrajByCarID(allData);
        log.trace("data read finish. {} cars", result.size());
        RawDataCleaning.assertTimeInc(result);
        log.trace("check time inc finish");
        return result;
    }

    private static List<GPSPoint> sample(int pointCount, List<GPSPoint> carData)
    {
        Set<Integer> sampleIndexSet = new HashSet<>();
        while(sampleIndexSet.size()<pointCount)
        {
            int index = random.nextInt(carData.size());
            sampleIndexSet.add(index);
        }
        List<GPSPoint> samplePoints = new ArrayList<>();
//        log.info("latitude\tlongitude\ttimestamp");
        for(Integer i : sampleIndexSet)
        {
            GPSPoint p = carData.get(i);
//            log.info("{}\t{}\t{}", p.getLatitude(), p.getLongitude(), p.getTimestamp());
            samplePoints.add(p);
        }
        return samplePoints;
    }

    private static boolean isUnique(int selectedCar, List<List<GPSPoint>> allRawData, List<GPSPoint> samplePoints, int timeInterval, int r)
    {

//        GPSPoint car = allRawData.get(selectedCar).get(0);
//        log.info("select car({}) id({}) index({})", car.getCarNo(), car.getCarID(), selectedCar);

        for(int i=0; i<allRawData.size(); i++)
        {
            if(i!=selectedCar)
            {
                List<GPSPoint> carData = allRawData.get(i);
                if(overlap(carData, samplePoints, timeInterval, r))
                {
                    return false;
                }
            }
        }
        return true;
    }

    private static class IsUnique extends Thread
    {

        private final int selectedCar;
        private final List<List<GPSPoint>> allRawData;
        private final List<GPSPoint> samplePoints;
        private final int timeInterval;
        private final int r;
        private final AtomicInteger uniqueCount;
        private final Semaphore semaphore;
        private final AtomicInteger completeCount;

        IsUnique(int selectedCar, List<List<GPSPoint>> allRawData, List<GPSPoint> samplePoints, int timeInterval, int r, AtomicInteger uniqueCount, Semaphore semaphore, AtomicInteger completeCount){
            this.selectedCar = selectedCar;
            this.allRawData = allRawData;
            this.samplePoints = samplePoints;
            this.timeInterval = timeInterval;
            this.r = r;
            this.uniqueCount = uniqueCount;
            this.semaphore = semaphore;
            this.completeCount = completeCount;
        }
        @Override
        public void run()
        {
            try {
                semaphore.acquire();
                for(int i=0; i<allRawData.size(); i++)
                {
                    if(i!=selectedCar)
                    {
                        List<GPSPoint> carData = allRawData.get(i);
                        if(overlap(carData, samplePoints, timeInterval, r))
                        {
                            return;
                        }
                    }
                }
                uniqueCount.getAndIncrement();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                semaphore.release();
                this.completeCount.getAndIncrement();
            }

        }
    }

//    private static float unicity(List<List<GPSPoint>> allRawData, int pointCount, int timeInterval, int r)
//    {
//
//        int uniqueCount = 0;
//
//        for(int i=0; i<allRawData.size(); i++)
//        {
//            List<GPSPoint> carData = allRawData.get(i);
//            List<GPSPoint> samples = sample(pointCount, carData);
//            if(isUnique(i, allRawData, samples, timeInterval, r))
//            {
//                uniqueCount++;
//            }
//        }
//        return uniqueCount * 1f / allRawData.size();
//    }

    private static float unicity(List<List<GPSPoint>> allRawData, int pointCount, int timeInterval, int r, int parallelCount)
    {
        Semaphore semaphore = new Semaphore(parallelCount);
        AtomicInteger uniqueCount = new AtomicInteger(0);
        AtomicInteger completeCount = new AtomicInteger(0);

        for(int i=0; i<allRawData.size(); i++)
        {
            List<GPSPoint> carData = allRawData.get(i);
            List<GPSPoint> samples = sample(pointCount, carData);
            new IsUnique(i, allRawData, samples, timeInterval, r, uniqueCount, semaphore, completeCount).start();
        }

        long lastTime = System.currentTimeMillis();

        while (completeCount.get()<allRawData.size()) {
            if (System.currentTimeMillis() - lastTime > 100_000) {
                lastTime = System.currentTimeMillis();
                log.debug("uniqueCount({}) remain({})", uniqueCount.get(), semaphore.getQueueLength());
                try {
                    Thread.sleep(10);
                } catch (InterruptedException ignore) {

                }
            }
        }

        return uniqueCount.get() * 1f / allRawData.size();
    }

    private static boolean overlap(List<GPSPoint> carData, List<GPSPoint> sampleIndexSet, int timeInterval, int r)
    {
        for(GPSPoint toMatch : sampleIndexSet)
        {
            if(!findOnePoint(carData, toMatch, timeInterval, r)){
                return false;
            }
        }
        return true;
    }

//    private static boolean findOnePoint(List<GPSPoint> carData, GPSPoint toMatch, int timeInterval, int r)
//    {
//        for (GPSPoint p : carData)
//        {
//            if (p.getTimestamp() > toMatch.getTimestamp()-timeInterval &&
//                    p.getTimestamp() < toMatch.getTimestamp()+timeInterval)
//            {
//                double distance = distanceCalc.calcDist(
//                        p.getLatitude(), p.getLongitude(),
//                        toMatch.getLatitude(), toMatch.getLongitude());
//                if(distance<2*r){
////                    log.info("match({}, {}, {}) is ({}, {}, {}), dis={}",
////                            toMatch.getTimestamp(),toMatch.getLatitude(), toMatch.getLongitude(),
////                            p.getTimestamp(), p.getLatitude(), p.getLongitude(),
////                            distance
////                            );
//                    return true;
//                }
//            }
//        }
//        return false;
//    }

    private static boolean findOnePoint(List<GPSPoint> carData, GPSPoint toMatch, int timeInterval, int r)
    {
        double[] geoBound = pointToBound(toMatch, 2*r);

        int[] index = binSearch(carData, toMatch.getTimestamp()-timeInterval, toMatch.getTimestamp()+timeInterval);

         for (int i=index[0];i<index[1]; i++)
        {
            GPSPoint p = carData.get(i);
            if (    geoBound[0] < p.getLat() && p.getLat() < geoBound[1]   &&
                    geoBound[2] < p.getLon() && p.getLon() < geoBound[3]   &&
                    p.getTimestamp() > toMatch.getTimestamp()-timeInterval &&
                    p.getTimestamp() < toMatch.getTimestamp()+timeInterval
                    )
            {
                double distance = distanceCalc.calcDist(
                        p.getLatitude(), p.getLongitude(),
                        toMatch.getLatitude(), toMatch.getLongitude());
                if(distance<2*r){
//                    log.info("match({}, {}, {}) is ({}, {}, {}), dis={}",
//                            toMatch.getTimestamp(),toMatch.getLatitude(), toMatch.getLongitude(),
//                            p.getTimestamp(), p.getLatitude(), p.getLongitude(),
//                            distance
//                            );
                    return true;
                }
            }
        }
        return false;
    }

    private static double[] pointToBound(GPSPoint p, double len) {
        double deltaRadian = len / 6371000.0D;
        double deltaDegree = Math.toDegrees(deltaRadian);
        double minLat = p.getLat() - deltaDegree;
        double maxLat = p.getLat() + deltaDegree;
        double minLon = p.getLon() - deltaDegree;
        double maxLon = p.getLon() + deltaDegree;
        return new double[]{minLat, maxLat, minLon, maxLon};
    }

    private static int[] binSearch(List<GPSPoint> carData, long lt, long rt) {
        if(lt>=rt) throw new RuntimeException("start time should less than end time! but "+lt+" >= "+rt);
        int i=0,j=carData.size();
        int[] index = new int[2];
        while(true){
            long mid = carData.get((i+j)/2).getTime();
            if(mid<lt){
                i=(i+j)/2;
            }else{ // mid >= lt
                j=(i+j)/2;
            }
            if(i==j || i+1==j) {
                break;
            }
        }
        index[0] = i; // left index

        j = carData.size();
        while(true){
            long mid = carData.get((i+j)/2).getTime();
            if(mid<=rt){
                i=(i+j)/2;
            }else{ // mid > lt
                j=(i+j)/2;
            }
            if(i==j || i+1==j) {
                break;
            }
        }
        index[1] = j; // right index
        return index;
    }

}
